/*******************************************************************************
 * Copyright (c) 2017-2020, org.smartboot. All rights reserved.
 * project name: smart-http
 * file name: HttpOutputStream.java
 * Date: 2020-01-01
 * Author: sandao (zhengjunweimail@163.com)
 ******************************************************************************/

package org.smartboot.http.server;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.Clock;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.Semaphore;

import org.smartboot.http.BufferOutputStream;
import org.smartboot.http.HttpRequest;
import org.smartboot.http.enums.HeaderNameEnum;
import org.smartboot.http.enums.HttpMethodEnum;
import org.smartboot.http.utils.Constant;
import org.smartboot.http.utils.HttpHeaderConstant;
import org.smartboot.socket.buffer.VirtualBuffer;
import org.smartboot.socket.transport.WriteBuffer;

/**
 * @author 三刀
 * @version V1.0 , 2018/2/3
 */
public abstract class AbstractOutputStream extends BufferOutputStream implements Reset {
	private static final byte[] CHUNKED_END_BYTES = "0\r\n\r\n".getBytes(StandardCharsets.US_ASCII);
	private static final SimpleDateFormat sdf = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH);
	private static final Semaphore flushDateSemaphore = new Semaphore(1);
	private static final Date currentDate = new Date(0);
	protected static byte[] DATE_SERVER_BYTES;

	private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("E, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH);

	private static String SERVER_ALIAS_NAME = "fast-server";

	static {
		String aliasServer = System.getProperty("smartHttp.server.alias");
		if (aliasServer != null) {
			SERVER_ALIAS_NAME += aliasServer;
		}
		flushDate();
	}

	protected final AbstractResponse response;
	protected final WriteBuffer writeBuffer;
	protected final HttpRequest request;
	protected boolean committed = false;
	/**
	 * 当前流是否完结
	 */
	protected boolean closed = false;
	protected boolean chunked = false;

	public AbstractOutputStream(HttpRequest request, AbstractResponse response, WriteBuffer writeBuffer) {
		this.response = response;
		this.request = request;
		this.writeBuffer = writeBuffer;
	}

	protected static void flushDate() {
		if ((System.currentTimeMillis() - currentDate.getTime() > 950) && flushDateSemaphore.tryAcquire()) {
			try {
				currentDate.setTime(System.currentTimeMillis());
				AbstractOutputStream.DATE_SERVER_BYTES = ("\r\n" + HttpHeaderConstant.Names.DATE + ":" + sdf.format(currentDate) + "\r\n" + HttpHeaderConstant.Names.SERVER + ":" + SERVER_ALIAS_NAME + "\r\n\r\n")
						.getBytes();
			} finally {
				flushDateSemaphore.release();
			}
		}
	}

	protected String getDate() {
		return ZonedDateTime.now(Clock.system(ZoneId.of("GMT"))).format(formatter);
	}

	protected String getServer() {
		return SERVER_ALIAS_NAME;
	}

	@Override
	public final void write(int b) {
		throw new UnsupportedOperationException();
	}

	/**
	 * 输出Http响应
	 *
	 * @param b
	 * @param off
	 * @param len
	 * @throws IOException
	 */
	@Override
	public final void write(byte b[], int off, int len) throws IOException {
		writeHead();
		if (HttpMethodEnum.HEAD.getMethod().equals(request.getMethod())) {
			throw new UnsupportedOperationException(request.getMethod() + " can not write http body");
		}
		if (chunked) {
			byte[] start = getBytes(Integer.toHexString(len) + "\r\n");
			writeBuffer.write(start);
			writeBuffer.write(b, off, len);
			writeBuffer.write(Constant.CRLF);
		} else {
			writeBuffer.write(b, off, len);
		}

	}

	public final void write(ByteBuffer buffer) throws IOException {
		write(VirtualBuffer.wrap(buffer));
	}

	@Override
	public final void write(VirtualBuffer virtualBuffer) throws IOException {
		writeHead();
		if (HttpMethodEnum.HEAD.getMethod().equals(request.getMethod())) {
			throw new UnsupportedOperationException(request.getMethod() + " can not write http body");
		}
		if (chunked) {
			byte[] start = getBytes(Integer.toHexString(virtualBuffer.buffer().remaining()) + "\r\n");
			writeBuffer.write(start);
			writeBuffer.write(virtualBuffer);
			writeBuffer.write(Constant.CRLF);
		} else {
			writeBuffer.write(virtualBuffer);
		}
	}

	/**
	 * 输出Http消息头
	 *
	 * @throws IOException
	 */
	abstract void writeHead() throws IOException;

	@Override
	public final void flush() throws IOException {
		writeHead();
		writeBuffer.flush();
	}

	@Override
	public final void close() throws IOException {
		if (closed) {
			throw new IOException("outputStream has already closed");
		}
		writeHead();

		if (chunked) {
			writeBuffer.write(CHUNKED_END_BYTES);
		}
		closed = true;
	}

	protected byte[] getHeaderNameBytes(String name) {
		HeaderNameEnum headerNameEnum = HttpHeaderConstant.HEADER_NAME_ENUM_MAP.get(name);
		if (headerNameEnum != null) {
			return headerNameEnum.getBytesWithColon();
		}

		byte[] extBytes = HttpHeaderConstant.HEADER_NAME_EXT_MAP.get(name);
		if (extBytes == null) {
			synchronized (name) {
				extBytes = getBytes("\r\n" + name + ":");
				HttpHeaderConstant.HEADER_NAME_EXT_MAP.put(name, extBytes);
			}
		}
		return extBytes;
	}

	protected final byte[] getBytes(String str) {
		return str.getBytes(StandardCharsets.US_ASCII);
	}

	public final void reset() {
		committed = closed = chunked = false;
	}

	public final boolean isClosed() {
		return closed;
	}
}
